from OpenGL import GL
import OpenGL.GL.ARB.gpu_shader_int64 as gsi64
import ctypes
import functools

from .helper import nitems, sizeof
from .GLObject import GLObject
from .GLInfo import GLInfo
from .GLConfig import GLConfig


def contex_check(func):
    @functools.wraps(func)
    def wraps(*args, **kwargs):
        self = args[0]
        cls = self.__class__
        if (
            self._vao.context != 0
            and self._vao.context != GLConfig.buffered_current_context
        ):
            if self._vao.context not in cls._cmd_buffer:
                cls._cmd_buffer[self._vao.context] = []
            cls._cmd_buffer[self._vao.context].append((func, args, kwargs))
            return

        return func(*args, **kwargs)

    return wraps


class VAP:

    _cmd_buffer = {}

    def __init__(self, vao, location):
        self._vao = vao
        self._location = location
        self._vbo = None
        self._element_type = None
        self._stride = 0
        self._offset = 0
        self._divisor = 0
        self._enabled = False

    @contex_check
    def interp(self, vbo, element_type, stride=0, offset=0):
        self._vao.bind()
        vbo.bind()
        if stride == 0:
            stride = sizeof(element_type)

        gl_type = GLInfo.dtype_inverse_map[element_type]
        if gl_type in [GL.GL_DOUBLE, gsi64.GL_UNSIGNED_INT64_ARB]:
            GL.glVertexAttribLPointer(
                self._location,
                nitems(element_type),
                gl_type,
                stride,
                ctypes.c_void_p(offset),
            )
        elif gl_type in GLInfo.int_types:
            GL.glVertexAttribIPointer(
                self._location,
                nitems(element_type),
                gl_type,
                stride,
                ctypes.c_void_p(offset),
            )
        else:
            GL.glVertexAttribPointer(
                self._location,
                nitems(element_type),
                gl_type,
                GL.GL_FALSE,
                stride,
                ctypes.c_void_p(offset),
            )
        self.enabled = True

        self._vbo = vbo
        self._element_type = element_type
        self._stride = stride
        self._offset = offset

    @property
    def element_type(self):
        return self._element_type

    @property
    def stride(self):
        return self._stride

    @property
    def offset(self):
        return self._offset

    @property
    def divisor(self):
        return self._divisor

    @property
    def enabled(self):
        return self._enabled

    @enabled.setter
    @contex_check
    def enabled(self, flag: bool):
        self._vao.bind()
        if flag:
            GL.glEnableVertexAttribArray(self._location)
        else:
            GL.glDisableVertexAttribArray(self._location)
        self._enabled = flag

    @divisor.setter
    @contex_check
    def divisor(self, value: int):
        if value < 0:
            raise ValueError("divisor should be integer that not less than 0")

        if self._divisor == value:
            return

        self._vao.bind()
        GL.glVertexAttribDivisor(self._location, value)
        self._divisor = value
        self.enabled = True


class VAO(GLObject):

    _basic_info = {
        "gen_func": GL.glGenVertexArrays,
        "bind_func": GL.glBindVertexArray,
        "del_func": GL.glDeleteVertexArrays,
        "target_type": None,
        "binding_type": GL.GL_VERTEX_ARRAY_BINDING,
        "need_number": True,
    }

    def __init__(self):
        GLObject.__init__(self, context_shared=False)
        self._ebo = None
        self._VAP_map = {}
        self._context: int = 0

    def __getitem__(self, location):
        if location not in self._VAP_map:
            self._VAP_map[location] = VAP(self, location)

        return self._VAP_map[location]

    def __contains__(self, location):
        return location in self._VAP_map

    @staticmethod
    def execute_cmd_buffer():
        current_context = GLConfig.buffered_current_context
        if current_context not in VAP._cmd_buffer:
            return

        for cmd in VAP._cmd_buffer[current_context]:
            cmd[0](*cmd[1], **cmd[2])

    def setEBO(self, ebo):
        if self._ebo is ebo:
            return

        self.bind()
        ebo.bind()
        self._ebo = ebo

    @property
    def ebo(self):
        return self._ebo

    @property
    def context(self):
        return self._context
